package com.hmdp.utils;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit){
        //用JSONUtil将其序列化为json对象，再以String形式进行存储
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    //set并带上逻辑过期
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        //注入基本的value
        redisData.setData(value);
        //注入逻辑过期时间，也就是当前时间+time，注意要把时间转换成秒
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        //这里写入Redis的序列是redisData序列化后的
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 解决缓存穿透，注意这里要做成泛型的
     * 我们需要的参数是形成key的前缀，id，以及传过来的对象
     * 这里的id也同样是泛型，因为没办法保证用户传过来的类型是Int还是Long
     * 这里如果不存在的话，我们就需要查询数据库，通用的函数根本不知道从数据库的哪张表进行查询，因此我们需要自行传入
     * Function<T, R>表示有参数有返回值的类型，“dbFallback”表示数据库降级逻辑，代表查询Redis失败后要去做的后备方案
     */
    public <R, ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        //从Redis中查询序列化的json字符串
        String json = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if (StrUtil.isNotBlank(json)) {
            //存在，直接返回
            return JSONUtil.toBean(json, type);
        }
        //注意isNotBlank会忽略null，所以还要看命中的是否是null
        if (json != null) {
            return null;
        }
        //不存在，根据id查询数据库
        R r = dbFallback.apply(id);
        //不存在，返回错误
        if (r == null){
            //存一个null到Redis中
            //这种没用的信息，TTL没必要设置太长了，这里我设置成了2min
            stringRedisTemplate.opsForValue().set(key, "", 2, TimeUnit.MINUTES);
            return null;
        }
        //存在，写入Redis，直接用set方法
        this.set(key, r, time, unit);
        //返回
        return r;
    }

    //逻辑过期解决缓存击穿问题
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public <ID, R> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type,Function<ID, R> dbFallback, Long time, TimeUnit unit){
        String key = CACHE_SHOP_KEY + id;
        //从Redis中查询商铺缓存，存储对象可以用String或者Hash，这里用String
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //判断是否存在
        if (StrUtil.isBlank(shopJson)) {
            //未命中，直接返回
            return null;
        }
        //命中，先把json反序列化成对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();//注意我们获取到了data信息以后，返回的会是一个JSONObject的格式
        R r = JSONUtil.toBean(data, type);
        LocalDateTime expireTime = redisData.getExpireTime();
        //判断是否过期
        if (expireTime.isAfter(LocalDateTime.now())){
            //未过期，直接返回店铺信息
            return r;
        }
        //已过期，缓存重建
        //获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;//LOCK_SHOP_KEY="lock:shop:"
        boolean isLock = tryLock(lockKey);
        //判断是否获取锁成功
        if (isLock){
            //成功获取锁，开启独立线程来实现缓存重建，用线程池来做
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 重建缓存
                    //查询数据库，这里依旧使用函数式编程
                    R r1 = dbFallback.apply(id);
                    //写入Redis
                    this.setWithLogicalExpire(key, r1, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //没有成功获取，直接返回过期信息
        return r;
    }

    private boolean tryLock(String key){
        //opsForValue里面没有真正的setNx，而是setIfAbsent，表示如果不存在就执行set
        //值就随便设定一下，重点是要获取到锁，但是设定了TTL为10s
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        /**
         * 如果是直接返回flag，可能会有拆箱操作，造成空指针，需要用BooleanUtil工具类
         * 因为Boolean不是基本类型的boolean，是boolean的封装类
         */
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
}
